package com.my.feign.config.fallBack;

import com.my.feign.config.fallBack.souCode.HystrixInvocationHandler;
import feign.Contract;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.hystrix.FallbackFactory;
import feign.hystrix.HystrixDelegatingContract;
import feign.hystrix.SetterFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * 自定义Feign.Builder，重写target(Target<T> target)，处理全局熔断。
 */
public final class XcloudFeign {


    private XcloudFeign() {}


    public static Builder builder() {
        return new Builder();
    }


    public static final class Builder extends Feign.Builder implements ApplicationContextAware {

        private Contract contract = new Contract.Default();

        private SetterFactory setterFactory = new SetterFactory.Default();

        private ApplicationContext applicationContext;

        private FeignContext feignContext;

        @Override
        public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Builder contract(Contract contract) {
            this.contract = contract;
            return this;
        }

        public Builder setterFactory(SetterFactory setterFactory) {
            this.setterFactory = setterFactory;
            return this;
        }

        @Override
        public <T> T target(Target<T> target) {
            // 自定义熔断器
            return (T) this.target(target, new GlobalFallbackFactory(target));
        }

        /**
         * @see #target(Class, String, Object)
         */
        public <T> T target(Target<T> target, T fallback) {
            return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
        }

        /**
         * @see #target(Class, String, FallbackFactory)
         */
        public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
            return build(fallbackFactory).newInstance(target);
        }

        /**
         * Like {@link Feign#newInstance(Target)}
         * fallback support.
         * @see #target(Target, Object)
         */
        public <T> T target(Class<T> apiType, String url, T fallback) {
            return target(new Target.HardCodedTarget<T>(apiType, url), fallback);
        }

        /**
         * Same as {@link #target(Class, String, T)}, except you can inspect a source exception before
         * creating a fallback object.
         */
        public <T> T target(Class<T> apiType,
                            String url,
                            FallbackFactory<? extends T> fallbackFactory) {
            return target(new Target.HardCodedTarget<T>(apiType, url), fallbackFactory);
        }

        @Override
        public Feign build() {
            return this.build(null);
        }

        /** Configures components needed for hystrix integration. */
        private Feign build(final FallbackFactory<?> defaultFallbackFactory) {
            super.invocationHandlerFactory(new InvocationHandlerFactory() {
                @Override
                public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
                    // using reflect get fallback and fallbackFactory properties from
                    // FeignClientFactoryBean because FeignClientFactoryBean is a package
                    // level class, we can not use it in our package
                    Object feignClientFactoryBean = Builder.this.applicationContext.getBean("&" + target.type().getName());

                    Class fallback = (Class) getFieldValue(feignClientFactoryBean, "fallback");
                    Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean, "fallbackFactory");
                    String beanName = (String) getFieldValue(feignClientFactoryBean, "contextId");
                    if (! StringUtils.hasText(beanName)) {
                        beanName = (String) getFieldValue(feignClientFactoryBean, "name");
                    }

                    // check fallback
                    if (void.class != fallback) {
                        Object fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
                        return new HystrixInvocationHandler(target, dispatch, setterFactory, new FallbackFactory.Default(fallbackInstance));
                    }

                    // check fallbackFactory
                    if (void.class != fallbackFactory) {
                        FallbackFactory fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);
                        return new HystrixInvocationHandler(target, dispatch, setterFactory, fallbackFactoryInstance);
                    }

                    // default fallbackFactory
                    return new HystrixInvocationHandler(target, dispatch, setterFactory, defaultFallbackFactory);
                }

                /* Get fallback or fallbackFactory from Context */
                private Object getFromContext(String name, String type, Class fallbackType, Class targetType) {
                    Object fallbackInstance = feignContext.getInstance(name, fallbackType);
                    if (fallbackInstance == null) {
                        throw new IllegalStateException(String.format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
                    }

                    if (! targetType.isAssignableFrom(fallbackType)) {
                        throw new IllegalStateException(String.format(
                                "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", type, fallbackType, targetType, name));
                    }
                    return fallbackInstance;
                }
            });
            super.contract(new HystrixDelegatingContract(contract));
            return super.build();
        }

        private Object getFieldValue(Object instance, String fieldName) {
            Field field = ReflectionUtils.findField(instance.getClass(), fieldName);
            field.setAccessible(true);
            try {
                return field.get(instance);
            }
            catch (IllegalAccessException e) {
                // ignore
            }
            return null;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
            feignContext = this.applicationContext.getBean(FeignContext.class);
        }
    }
}

